package be.kdg.bombermanunlimited.client.network;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import be.kdg.bombermanunlimited.client.CentralRegistry;
import be.kdg.bombermanunlimited.client.CentralRegistry.BombermanObjects;
import be.kdg.bombermanunlimited.client.network.listeners.ErrorListener;
import be.kdg.bombermanunlimited.client.network.listeners.INetworkDataListener;
import be.kdg.bombermanunlimited.client.network.listeners.INetworkListener;
import be.kdg.bombermanunlimited.client.network.listeners.IRingServerListener;
import be.kdg.bombermanunlimited.client.network.listeners.ISocketStateListener;
import be.kdg.bombermanunlimited.client.network.packet.Packet;
import be.kdg.bombermanunlimited.client.network.sockets.EventSocket;
import be.kdg.bombermanunlimited.server.network.interfaces.INetworkManager;

public class RingNetwork implements INetwork, INetworkDataListener,
		ISocketStateListener {
	// the default port of the ring network
	public static final int DEFAULT_PORT = 2621;

	// a list of sockets, normally a ring has only 2 sockets
	// this will give problems with firewalls etc, so we can create more
	// outgoing
	// and incomming connections
	private List<EventSocket> sockets;
	private ServerSocket listenSocket;
	// listeners for the network, when a new connections is established etc
	private List<INetworkListener> networkListeners;
	// the data listeners
	// everey listeners can listen to a list of Packets
	// the listener will only called when the receved packettype is in the list
	private Map<INetworkDataListener, List<Object>> dataListeners;
	// the network manager of the server
	private INetworkManager manager;

	private RingNetworkServerListener ringListener;

	private List<ErrorListener> errorListeners;

	private int listenPort;

	private boolean hasConnection;

	public RingNetwork(int listenPort, INetworkManager manager,
			ErrorListener el, INetworkListener nl) {
		networkListeners = new ArrayList<INetworkListener>();
		dataListeners = new HashMap<INetworkDataListener, List<Object>>();
		sockets = new ArrayList<EventSocket>();
		errorListeners = new ArrayList<ErrorListener>();

		hasConnection = false;

		if (el != null) {
			// System.out.println("[RingNetwork] error listener added");
			errorListeners.add(el);
		}

		if (nl != null) {
			networkListeners.add(nl);
		}

		this.manager = manager;
		this.listenPort = listenPort;
		CentralRegistry reg = CentralRegistry.getRegistry();

		// register listener to the manager
		// this way the server can tell the client to connect with another node
		try {
			ringListener = new RingNetworkServerListener();

			manager.registerNode(reg.getString(BombermanObjects.LOCAL_IP),
					listenPort, ringListener);
		} catch (RemoteException e1) {
			e1.printStackTrace();
		}
	}

	@Override
	public synchronized void connect(String ip, int port)
			throws UnknownHostException, IOException {
		// create new socket, connect to it and listen to it
		// System.out.println("[RingNetwork] Connecting to " + ip + ":" + port);
		EventSocket socket = new EventSocket(ip, port, this, this);
		sockets.add(socket);
		// System.out.println("[RingNetwork] Connected to " + ip + ":" + port);

		onConnect(socket);
	}

	@Override
	public synchronized void disconnect() {
		if (hasConnection) {
			try {
				// System.out.println("[RingNetwork] Disconnecting");

				// remove the node from the server
				manager.removeNode(
						CentralRegistry.getRegistry().getString(
								BombermanObjects.LOCAL_IP), listenPort,
						ringListener);
			} catch (RemoteException e1) {
				e1.printStackTrace();
			}
		}

		// close all sockets
		for (EventSocket socket : sockets) {
			try {
				socket.disconnect();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		sockets.clear();

		if (listenSocket != null) {
			try {
				listenSocket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}

			listenSocket = null;
		}

		for (INetworkListener l : networkListeners) {
			l.onDisconnect();
		}
	}

	@Override
	public synchronized boolean hasConnections() {
		// check if there are connection open
		int index = 0;
		while (index < sockets.size() && !sockets.get(index).isConnected()) {
			index++;
		}

		return index < sockets.size();
	}

	@Override
	public synchronized void addDataListener(INetworkDataListener listener,
			Class<? extends Packet> packetType) {
		if (dataListeners.get(listener) == null) {
			dataListeners.put(listener, new ArrayList<Object>());
		}

		dataListeners.get(listener).add(packetType);
	}

	@Override
	public synchronized void removeDataListener(INetworkDataListener listener) {
		dataListeners.remove(listener);
	}

	@Override
	public synchronized void removeDataListener(INetworkDataListener listener,
			Class<? extends Packet> packetType) {
		dataListeners.get(listener).remove(packetType);
	}

	@Override
	public synchronized void sendData(Packet packet) {
		onIncomingData(null, packet);
		int index = 0;
		while (index < sockets.size()) {
			EventSocket socket = sockets.get(index);
			if (sendData(socket, packet)) {
				index++;
			} else {
				sockets.remove(index);
			}
		}
	}

	private synchronized boolean sendData(EventSocket socket, Packet packet) {
		try {
			// System.out.println("[RingNetwork] sending data: " +
			// packet.getDataLength());
			socket.sendPacket(packet);
		} catch (IOException e) {
			CentralRegistry reg = CentralRegistry.getRegistry();
			try {
				manager.connectionLost(
						reg.getString(BombermanObjects.LOCAL_IP),
						reg.getInteger(BombermanObjects.RING_PORT), socket
								.getRemoteAddress().getAddress()
								.getHostAddress(),
						socket.getRemoteRegisteredPort());
			} catch (RemoteException e1) {
			}

			return false;
		}

		return true;
	}

	@Override
	public synchronized void onIncomingData(InetSocketAddress source,
			Packet packet) {
		// System.out.println("[RingNetwork] incoming data: " +
		// packet.getDataLength());
		for (INetworkDataListener l : dataListeners.keySet()) {
			if (dataListeners.get(l).contains(packet.getClass())) {
				l.onIncomingData(source, packet);
			}
		}

		// if source == null, then it is a packet that comes from
		// this application itself, so don't rebroadcast it!
		if (source != null) {
			// System.out.println("[RingNetwork] Resending data");
			for (EventSocket socket : sockets) {
				if (!socket.getRemoteAddress().equals(source)) {
					sendData(socket, packet);
				}
			}
		}

		// System.out.println("[RingNetwork] Resending end");
	}

	@Override
	public synchronized void addNetworkListener(INetworkListener listener) {
		networkListeners.add(listener);
	}

	@Override
	public synchronized void removeNetworkListener(INetworkListener listener) {
		networkListeners.remove(listener);
	}

	@Override
	public synchronized void onConnect(EventSocket source) {
		for (INetworkListener l : networkListeners) {
			l.onConnect();
		}
	}

	@Override
	public synchronized void onDisconnect(EventSocket source) {
		sockets.remove(source);

		if (!hasConnections()) {
			for (INetworkListener l : networkListeners) {
				l.onDisconnect();
			}
		}
	}

	/**
	 * Geef de server een seintje dat er een verbinding onverwachts is verbroken
	 */
	@Override
	public synchronized void onConnectionLost(EventSocket source) {
		// System.out.println("[RingNetwork] Connection lost to " +
		// source.getRemoteAddress().getAddress().getHostAddress() + ":" +
		// source.getRemoteRegisteredPort());
		sockets.remove(source);

		try {
			manager.connectionLost(
					CentralRegistry.getRegistry().getString(
							BombermanObjects.LOCAL_IP), listenPort, source
							.getRemoteAddress().getAddress().getHostAddress(),
					source.getRemoteRegisteredPort());

		} catch (RemoteException e) {
			e.printStackTrace();
		}

		if (!hasConnections()) {
			for (INetworkListener l : networkListeners) {
				l.onConnectionLost();
			}
		}
	}

	/**
	 * Start met het luisteren naar nieuwe connecties.
	 */
	private synchronized void startListening() {
		if (listenSocket == null) {
			// System.out.println("[RingNetwork] Start listening");
			try {
				CentralRegistry reg = CentralRegistry.getRegistry();
				listenSocket = new ServerSocket(listenPort, 10,
						InetAddress.getByName(reg
								.getString(BombermanObjects.LOCAL_IP)));
				new Thread(new Runnable() {
					@Override
					public void run() {
						try {
							while (true) {
								Socket tmp = listenSocket.accept();
								// accept the socket
								EventSocket socket = new EventSocket(tmp,
										RingNetwork.this, RingNetwork.this);
								// add the socket to the list
								sockets.add(socket);
								// System.out.println("Server socket connected!");

								onConnect(socket);
							}
						} catch (IOException e) {
						}
					}
				}).start();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Een classe die de ServerListener implementeerd zodat de server commando's
	 * kan sturen naar de client via rmi.
	 * 
	 * @author Philip
	 * 
	 */
	public class RingNetworkServerListener extends UnicastRemoteObject
			implements IRingServerListener {

		/**
		 * 
		 */
		private static final long serialVersionUID = 7550556674588845286L;

		protected RingNetworkServerListener() throws RemoteException {
			super();
		}

		@Override
		public void connectTo(String ip, int port) throws RemoteException {
			if (port != -1) {
				try {
					connect(ip, port);
				} catch (UnknownHostException e) {
					manager.cantConnect(CentralRegistry.getRegistry()
							.getString(BombermanObjects.LOCAL_IP), listenPort,
							ip, port);
					e.printStackTrace();
				} catch (IOException e) {
					manager.cantConnect(CentralRegistry.getRegistry()
							.getString(BombermanObjects.LOCAL_IP), listenPort,
							ip, port);
					e.printStackTrace();
				}
			} else {
				System.out
						.println("[RingNetwork:RingNetworkServerListener]: No node found in the list to connect with!");
			}
		}

		@Override
		public boolean isUp() throws RemoteException {
			return true;
		}

		@Override
		public void socketAlreadyInUse() throws RemoteException {
			// System.out.println("[RingNetwork] Socket in use");
			hasConnection = false;
			for (ErrorListener l : errorListeners) {
				l.socketInUse();
			}
		}

		@Override
		public void addSuccess() throws RemoteException {
			hasConnection = true;
			startListening();

			// System.out.println("[RingNetwork] node added");
			for (int i = 0; i < networkListeners.size(); i++) {
				networkListeners.get(i).nodeAdded();
			}
		}

		@Override
		public void serverFull() throws RemoteException {
			hasConnection = false;
			for (ErrorListener l : errorListeners) {
				l.serverFull();
			}
		}
	}

	@Override
	public synchronized void addErrorListener(ErrorListener listener) {
		errorListeners.add(listener);
	}

	@Override
	public synchronized void removeErrorListener(ErrorListener listener) {
		errorListeners.remove(listener);
	}
}
